package net.sourceforge.pmd.eclipse.plugin;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

import net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.RuleSet;
import net.sourceforge.pmd.RuleSetFactory;
import net.sourceforge.pmd.RuleSetNotFoundException;
import net.sourceforge.pmd.eclipse.core.IRuleSetManager;
import net.sourceforge.pmd.eclipse.core.ext.RuleSetsExtensionProcessor;
import net.sourceforge.pmd.eclipse.core.impl.RuleSetManagerImpl;
import net.sourceforge.pmd.eclipse.runtime.cmd.JavaProjectClassLoader;
import net.sourceforge.pmd.eclipse.runtime.preferences.IPreferences;
import net.sourceforge.pmd.eclipse.runtime.preferences.IPreferencesFactory;
import net.sourceforge.pmd.eclipse.runtime.preferences.IPreferencesManager;
import net.sourceforge.pmd.eclipse.runtime.preferences.impl.PreferencesFactoryImpl;
import net.sourceforge.pmd.eclipse.runtime.properties.IProjectProperties;
import net.sourceforge.pmd.eclipse.runtime.properties.IProjectPropertiesManager;
import net.sourceforge.pmd.eclipse.runtime.properties.IPropertiesFactory;
import net.sourceforge.pmd.eclipse.runtime.properties.PropertiesException;
import net.sourceforge.pmd.eclipse.runtime.properties.impl.PropertiesFactoryImpl;
import net.sourceforge.pmd.eclipse.runtime.writer.IAstWriter;
import net.sourceforge.pmd.eclipse.runtime.writer.IRuleSetWriter;
import net.sourceforge.pmd.eclipse.runtime.writer.impl.WriterFactoryImpl;
import net.sourceforge.pmd.eclipse.ui.RuleLabelDecorator;
import net.sourceforge.pmd.eclipse.ui.ShapePainter;
import net.sourceforge.pmd.eclipse.ui.nls.StringKeys;
import net.sourceforge.pmd.eclipse.ui.nls.StringTable;
import net.sourceforge.pmd.eclipse.util.ResourceManager;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.java.JavaLanguageModule;

import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.RollingFileAppender;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IDecoratorManager;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

/**
 * The activator class controls the plug-in life cycle
 */
public class PMDPlugin extends AbstractUIPlugin {

    private static File pluginFolder;

    private FileChangeReviewer changeReviewer;

    private Map<RGB, Color> coloursByRGB = new HashMap<RGB, Color>();

    public static final String PLUGIN_ID = "net.sourceforge.pmd.eclipse.plugin";

    private static Map<IProject, IJavaProject> JavaProjectsByIProject = new HashMap<IProject, IJavaProject>();

    // The shared instance
    private static PMDPlugin plugin;

    public static String VERSION = "unknown";

    private static final Integer[] priorityValues = new Integer[] { Integer.valueOf(1), Integer.valueOf(2),
            Integer.valueOf(3), Integer.valueOf(4), Integer.valueOf(5) };

    private static final Logger log = Logger.getLogger(PMDPlugin.class);

    private StringTable stringTable; // NOPMD by Herlin on 11/10/06 00:22

    public static final String ROOT_LOG_ID = "net.sourceforge.pmd";
    private static final String PMD_ECLIPSE_APPENDER_NAME = "PMDEclipseAppender";
    private IPreferencesFactory preferencesFactory = new PreferencesFactoryImpl();
    private IPropertiesFactory propertiesFactory = new PropertiesFactoryImpl();

    private final IRuleSetManager ruleSetManager = new RuleSetManagerImpl(); // NOPMD:SingularField

    /**
     * The constructor
     */
    public PMDPlugin() {
    }

    public Color colorFor(RGB rgb) {

        Color color = coloursByRGB.get(rgb);
        if (color != null)
            return color;

        color = new Color(null, rgb.red, rgb.green, rgb.blue);
        coloursByRGB.put(rgb, color);

        return color;
    }

    public static void setJavaClassLoader(PMDConfiguration config, IProject project) {

        IPreferences preferences = getDefault().loadPreferences();
        try {
            if (preferences.isProjectBuildPathEnabled() && project.hasNature(JavaCore.NATURE_ID)) {
                config.setClassLoader(new JavaProjectClassLoader(config.getClass().getClassLoader(), project));
            }
        } catch (CoreException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Return the Java language version for the resources found within the
     * specified project or null if it isn't a Java project or a Java version we
     * don't support yet.
     * 
     * @param project
     * @return
     */
    public static LanguageVersion javaVersionFor(IProject project) {

        IJavaProject jProject = JavaProjectsByIProject.get(project);
        if (jProject == null) {
            jProject = JavaCore.create(project);
            JavaProjectsByIProject.put(project, jProject);
        }

        if (jProject.exists()) {
            String compilerCompliance = jProject.getOption(JavaCore.COMPILER_COMPLIANCE, true);
            return LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getVersion(compilerCompliance);
        }
        return null;
    }

    public static IClasspathEntry buildSourceClassPathEntryFor(IProject project) {
        IJavaProject jProject = JavaProjectsByIProject.get(project);
        if (jProject == null) {
            jProject = JavaCore.create(project);
            JavaProjectsByIProject.put(project, jProject);
        }
        if (jProject.exists()) {
            try {
                if (jProject.getRawClasspath() != null) {
                    for (IClasspathEntry entry : jProject.getRawClasspath()) {
                        if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
                            return entry;
                        }
                    }
                }
            } catch (JavaModelException e) {
                log.error("Couldn't determine source classpath", e);
            }
        }
        return null;
    }

    private void disposeResources() {

        disposeAll(coloursByRGB.values());
    }

    public static void disposeAll(Collection<Color> colors) {
        for (Color color : colors)
            color.dispose();
    }

    public static File getPluginFolder() {

        if (pluginFolder == null) {
            URL url = Platform.getBundle(PLUGIN_ID).getEntry("/");
            try {
                url = FileLocator.resolve(url);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            pluginFolder = new File(url.getPath());
        }

        return pluginFolder;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext
     * )
     */
    public void start(BundleContext context) throws Exception {
        super.start(context);
        plugin = this;

        // this needs to be executed before the preferences are loaded, because
        // the standard
        // rulesets are needed for the default active rules.
        registerStandardRuleSets();

        IPreferences prefs = loadPreferences();
        configureLogs(prefs);
        registerAdditionalRuleSets();
        fileChangeListenerEnabled(prefs.isCheckAfterSaveEnabled());

        // if a project is deleted, remove the cached project properties
        ResourcesPlugin.getWorkspace().addResourceChangeListener(new IResourceChangeListener() {
            public void resourceChanged(IResourceChangeEvent arg0) {
                if (arg0.getType() == IResourceChangeEvent.PRE_DELETE && arg0.getResource() instanceof IProject) {
                    getPropertiesManager().removeProjectProperties((IProject) arg0.getResource());
                }
            }
        });

        VERSION = context.getBundle().getHeaders().get("Bundle-Version");
    }

    public void fileChangeListenerEnabled(boolean flag) {

        IWorkspace workspace = ResourcesPlugin.getWorkspace();

        if (flag) {
            if (changeReviewer == null)
                changeReviewer = new FileChangeReviewer();
            workspace.addResourceChangeListener(changeReviewer);
        } else {
            if (changeReviewer != null) {
                workspace.removeResourceChangeListener(changeReviewer);
                changeReviewer = null;
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext
     * )
     */
    public void stop(BundleContext context) throws Exception {

        fileChangeListenerEnabled(false);

        plugin = null;
        disposeResources();
        ShapePainter.disposeAll();
        ResourceManager.dispose();
        super.stop(context);
    }

    /**
     * Returns the shared instance
     *
     * @return the shared instance
     */
    public static PMDPlugin getDefault() {
        return plugin;
    }

    /**
     * Returns an image descriptor for the image file at the given plug-in
     * relative path.
     *
     * @param path
     *            the path
     * @return the image descriptor
     */
    public static ImageDescriptor getImageDescriptor(String path) {
        return AbstractUIPlugin.imageDescriptorFromPlugin("net.sourceforge.pmd.eclipse.plugin", path);
    }

    /**
     * Get an image corresponding to the severity
     */
    public Image getImage(String key, String iconPath) {
        ImageRegistry registry = getImageRegistry();
        Image image = registry.get(key);
        if (image == null) {
            ImageDescriptor descriptor = getImageDescriptor(iconPath);
            if (descriptor != null) {
                registry.put(key, descriptor);
                image = registry.get(key);
            }
        }

        return image;
    }

    /**
     * Helper method to log error
     *
     * @see IStatus
     */
    public void logError(String message, Throwable t) {
        getLog().log(new Status(IStatus.ERROR, getBundle().getSymbolicName(), 0, message + t.getMessage(), t));
        if (log != null) {
            log.error(message, t);
        }
    }

    /**
     * Helper method to log error
     *
     * @see IStatus
     */
    public void logError(IStatus status) {
        getLog().log(status);
        if (log != null) {
            log.error(status.getMessage(), status.getException());
        }
    }

    /**
     * Helper method to display error
     */
    public void showError(final String message, final Throwable t) {
        logError(message, t);
        Display.getDefault().syncExec(new Runnable() {

            public void run() {
                String errTitle = getStringTable().getString(StringKeys.ERROR_TITLE);
                MessageDialog.openError(Display.getCurrent().getActiveShell(), errTitle,
                        message + "\n" + String.valueOf(t));
            }
        });
    }

    /**
     * Helper method to display a non-logged user error
     */
    public void showUserError(final String message) {

        Display.getDefault().syncExec(new Runnable() {

            public void run() {
                String errTitle = getStringTable().getString(StringKeys.ERROR_TITLE);
                MessageDialog.openError(Display.getCurrent().getActiveShell(), errTitle, message);
            }
        });
    }

    /**
     * @return an instance of the string table
     */
    public StringTable getStringTable() {
        if (stringTable == null) {
            stringTable = new StringTable();
        }

        return stringTable;
    }

    /**
     * @return the priority values
     * @deprecated
     */
    public Integer[] getPriorityValues() {
        return priorityValues;
    }

    /**
     * Load the PMD plugin preferences
     */
    public IPreferences loadPreferences() {
        return getPreferencesManager().loadPreferences();
    }

    /**
     * @return the plugin preferences manager
     */
    public IPreferencesManager getPreferencesManager() {
        return preferencesFactory.getPreferencesManager();
    }

    /**
     * @return the plugin project properties manager
     */
    public IProjectPropertiesManager getPropertiesManager() {
        return propertiesFactory.getProjectPropertiesManager();
    }

    /**
     * @param project
     *            a workspace project
     * @return the PMD properties for that project
     */
    public IProjectProperties loadProjectProperties(IProject project) throws PropertiesException {
        return getPropertiesManager().loadProjectProperties(project);
    }

    /**
     * Helper method to log information
     *
     * @see IStatus
     */
    public void logInformation(String message) {
        getLog().log(new Status(IStatus.INFO, getBundle().getSymbolicName(), 0, message, null));
    }

    public void logWarn(String message) {
        getLog().log(new Status(IStatus.WARNING, getBundle().getSymbolicName(), 0, message, null));
    }

    /**
     * @return an instance of an AST writer
     */
    public IAstWriter getAstWriter() {
        return new WriterFactoryImpl().getAstWriter();
    }

    /**
     * @return an instance of a ruleset writer
     */
    public IRuleSetWriter getRuleSetWriter() {
        return new WriterFactoryImpl().getRuleSetWriter();
    }

    /**
     * Apply the log preferences
     */
    public void applyLogPreferences(IPreferences preferences) {
        Logger log = Logger.getLogger(ROOT_LOG_ID);
        log.setLevel(preferences.getLogLevel());
        RollingFileAppender appender = (RollingFileAppender) log.getAppender(PMD_ECLIPSE_APPENDER_NAME);
        if (appender == null) {
            configureLogs(preferences);
        } else if (!appender.getFile().equals(preferences.getLogFileName())) {
            appender.setFile(preferences.getLogFileName());
            appender.activateOptions();
        }
    }

    /**
     * Configure the logging
     *
     */
    private void configureLogs(IPreferences preferences) {
        try {
            Layout layout = new PatternLayout("%d{yyyy/MM/dd HH:mm:ss,SSS} %-5p %-32c{1} %m%n");

            RollingFileAppender appender = new RollingFileAppender(layout, preferences.getLogFileName());
            appender.setName(PMD_ECLIPSE_APPENDER_NAME);
            appender.setMaxBackupIndex(1);
            appender.setMaxFileSize("10MB");

            Logger.getRootLogger().addAppender(new ConsoleAppender(layout));
            Logger.getRootLogger().setLevel(Level.WARN);
            Logger.getRootLogger().setAdditivity(false);

            Logger.getLogger(ROOT_LOG_ID).addAppender(appender);
            Logger.getLogger(ROOT_LOG_ID).setLevel(preferences.getLogLevel());
            Logger.getLogger(ROOT_LOG_ID).setAdditivity(false);

        } catch (IOException e) {
            logError("IO Exception when configuring logging.", e);
        }
    }

    /**
     * @return the ruleset manager instance
     */
    public final IRuleSetManager getRuleSetManager() {
        return ruleSetManager;
    }

    /**
     * Logs inside the Eclipse environment
     *
     * @param severity
     *            the severity of the log (IStatus code)
     * @param message
     *            the message to log
     * @param t
     *            a possible throwable, may be null
     */
    public final void log(final int severity, final String message, final Throwable t) {
        final Bundle bundle = getBundle();
        if (bundle != null) {
            getLog().log(new Status(severity, bundle.getSymbolicName(), 0, message, t));
        }

        // TODO : when bundle is not created yet (ie at startup), we cannot log
        // ; find a way to log.
    }

    /**
     * Registering the standard rulesets
     *
     */
    private void registerStandardRuleSets() {

        final RuleSetFactory factory = new RuleSetFactory();
        try {
            Iterator<RuleSet> iterator = factory.getRegisteredRuleSets();
            final IRuleSetManager manager = getRuleSetManager();
            RuleSet ruleSet;
            while (iterator.hasNext()) {
                ruleSet = iterator.next();
                manager.registerRuleSet(ruleSet);
                manager.registerDefaultRuleSet(ruleSet);
            }
        } catch (RuleSetNotFoundException e) {
            log(IStatus.WARNING, "Problem getting all registered PMD RuleSets", e);
        }
    }

    /**
     * Register additional rulesets that may be provided by a fragment. Find
     * extension points implementation and call them
     *
     */
    private void registerAdditionalRuleSets() {
        try {
            final RuleSetsExtensionProcessor processor = new RuleSetsExtensionProcessor(getRuleSetManager());
            processor.process();
        } catch (CoreException e) {
            log(IStatus.ERROR, "Error when processing RuleSets extensions", e);
        }
    }

    public RuleLabelDecorator ruleLabelDecorator() {
        IDecoratorManager mgr = getWorkbench().getDecoratorManager();
        // TODO don't use a raw string...urgh
        return (RuleLabelDecorator) mgr.getBaseLabelProvider("net.sourceforge.pmd.eclipse.plugin.RuleLabelDecorator");
    }

    public void changedFiles(Collection<IFile> changedFiles) {

        RuleLabelDecorator rld = ruleLabelDecorator();
        if (rld == null)
            return;

        Collection<IResource> withParents = new HashSet<IResource>(changedFiles.size() * 2);
        withParents.addAll(changedFiles);
        for (IFile file : changedFiles) {
            IResource parent = file.getParent();
            while (parent != null) {
                withParents.add(parent);
                parent = parent.getParent();
            }
        }

        rld.changed(withParents);
    }

    private void addFilesTo(IResource resource, Collection<IResource> allKids) {

        if (resource instanceof IFile) {
            allKids.add(resource);
            return;
        }

        if (resource instanceof IFolder) {
            IFolder folder = (IFolder) resource;
            IResource[] kids = null;
            try {
                kids = folder.members();
            } catch (CoreException e) {
                e.printStackTrace();
            }
            addKids(allKids, kids);

            allKids.add(folder);
            return;
        }

        if (resource instanceof IProject) {
            IProject project = (IProject) resource;
            IResource[] kids = null;
            try {
                kids = project.members();
            } catch (CoreException e) {
                e.printStackTrace();
            }
            addKids(allKids, kids);
            allKids.add(project);
            return;
        }
    }

    private void addKids(Collection<IResource> allKids, IResource[] kids) {

        if (kids == null)
            return;

        for (IResource irc : kids) {
            if (irc instanceof IFile) {
                allKids.add(irc);
                continue;
            }
            if (irc instanceof IFolder) {
                addFilesTo(irc, allKids);
            }
        }
    }

    public void removedMarkersIn(IResource resource) {

        RuleLabelDecorator decorator = ruleLabelDecorator();
        if (decorator == null)
            return;

        Collection<IResource> changes = new ArrayList<IResource>();

        addFilesTo(resource, changes);

        decorator.changed(changes);
    }

}
